昨天建構的服務還有些缺點:
查詢官方文件,確定應該用哪些 validation rule。
<?php
// app/Http/Controller/ShorterController.php
// ...
public function __invoke(Request $request)
{
$request->validate(['url' => 'required|url']);
// ...
}
redirect()->to($url)
時,若 $url
不是一個合法的 URL(非以 http 開頭的)會被視為路徑。先看一下原本的程式碼
<?php
// app/Http/Controller/RedirectController.php
public function __invoke(int $id): RedirectResponse
{
$url = Url::find($id)->url;
return redirect()->to($url);
}
如果今天用戶傳進來的 $id = 1000000
,而在資料庫中並沒有這個值。
Url::find($id)
會回傳 null
null->url
會直接產生錯誤此時,我們有幾種方法可以避免發生這種狀況
<?php
// app/Http/Controller/RedirectController.php
public function __invoke(int $id): RedirectResponse
{
$url = Url::findOrFail($id)->url;
return redirect()->to($url);
}
當 Url::findOrFail()
找不到指定的 ID 時,會直接丟出 Illuminate\Database\Eloquent\ModelNotFoundException
,此時Laravel 會直接回應 404。
首先修改 Route,將 id
改為 url
<?php
// routes/web.php
Route::get('/{url}', 'RedirectController');
接著修改 Controller
<?php
// app/Http/Controllers/RedirectController.php
use App/Url;
class RedirectController extends Controller
{
public function __invoke(Url $url)
{
return $url->url;
}
}
得益於 Illuminate\Routing\Middleware\SubstituteBindings
這個中介層,它可以自動將路由參數轉為指定的 Model,而且當 Model 不存在時直接回應 404。
目前的短網址型式是 [http://127.0.0.1/{id}](http://127.0.0.1/{id})
,很容易被遍歷 ID 以取得系統上所有的短網址。
我們需要有一個方法將 ID 轉為難以被逆推的字串,此時建議可以使用 Hashids 這個演算法,在 Laravel 中有 vinkla/laravel-hashids 這個套件。
$ composer require vinkla/hashids
<?php
// app/Http/Controllers/ShorterController.php
// ...
use Vinkla\Hashids\Facades\Hashids;
public function __invoke(Request $request)
{
$request->validate(['url' => 'required|url']);
$url = Url::create(['url' => $request->url]);
return Hashids::encode($url->id);
}
<?php
// app/Http/Controllers/RedirectController.php
// ...
use Vinkla\Hashids\Facades\Hashids;
public function __invoke(string $hashedIds)
{r
$url = Url::findOrFail(Hashids::decode($hashedIds)[0] ?? null);
return $url->url;
}
上面我有提到可以利用路由模型綁定的功能,那加入 Hashids 之後是否仍然可以自動綁定呢?
可以的,只要在 Eloquent ORM 上改寫 resolveRouteBinding()
就可以自定義解析邏輯。
<?php
// app/Url.php
// ...
class Url extends Model
{
// ...
public function resolveRouteBinding($value, $field = null)
{
return $this->find(Hashids::decode($value)[0] ?? null);
}
}